feat(config): unified CODEFRAME.md workflow config (#398)#445
Conversation
) Steps 1 & 2 of issue #398: Add CodeframeConfig dataclass, parse_codeframe_md() function for YAML front matter + markdown body, integrate into load_preferences() with CODEFRAME.md as highest priority, and add get_codeframe_config() accessor. 15 new tests, all 31 existing tests pass with no regressions.
…steps 3-4) Step 3: Add --generate-config flag to cf init that creates a starter CODEFRAME.md with YAML front matter (engine, tech_stack, gates, batch, agent settings) and a markdown body with project instructions. Step 4: Update load_environment_config() to fall back to CODEFRAME.md front matter when .codeframe/config.yaml is absent, with conversion from CodeframeConfig to EnvironmentConfig.
WalkthroughAdds unified CODEFRAME.md support (YAML front matter + Markdown body), a new Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as CLI (app.py)
participant Generator as Config Generator
participant FS as Filesystem
participant Loader as Preference Loader
User->>CLI: cf init --generate-config
CLI->>Generator: init(generate_config=true)
Generator->>Generator: detect tech_stack & gates
Generator->>FS: write CODEFRAME.md (YAML + body)
FS-->>User: CODEFRAME.md created
User->>Loader: load_preferences(workspace)
Loader->>FS: locate CODEFRAME.md / AGENTS.md / CLAUDE.md
FS-->>Loader: return CODEFRAME.md (if found)
Loader->>Loader: parse_codeframe_md(content) -> CodeframeConfig + AgentPreferences
Loader-->>User: preferences/config returned
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@codeframe/cli/app.py`:
- Around line 353-362: The current gate inference uses an if/elif chain so only
the first match is emitted; modify the logic around tech_stack/ts_lower and
gates so each detected runtime/framework adds its gates independently (replace
the if/elif with separate if checks), and add checks for plain JavaScript
indicators like "javascript" or "node" (so eslint/jest are added when TypeScript
isn't mentioned). Ensure you still append results into the gates list and assign
yaml_section["gates"] when non-empty.
- Around line 180-187: The current generation code unconditionally overwrites
CODEFRAME.md; modify the logic around generate_config/_generate_codeframe_md so
that before calling config_path.write_text you check if (repo_path /
"CODEFRAME.md").exists() and if so fail fast (raise an error or console.print
and exit) unless an explicit force flag is provided; add or wire a boolean force
parameter (e.g., --force or force_generate) into the CLI handler and use it in
this branch to allow overwriting when true, otherwise do not clobber the
existing file and return a clear message referencing CODEFRAME.md and the
--force option.
In `@codeframe/core/config.py`:
- Around line 399-403: The hook keys produced by parse_codeframe_md (e.g.,
after_task, on_failure) are not being normalized before creating HooksConfig, so
documented hooks get dropped; update the cf_config.hooks handling to first
normalize/translate documented hook names to the actual HooksConfig field names
(for example by building a mapping from documented names -> dataclass field
names or by applying a deterministic rename function), then filter using
HooksConfig.__dataclass_fields__ and instantiate config.hooks =
HooksConfig(**filtered); touch the cf_config.hooks branch and use the
HooksConfig symbol and parse_codeframe_md examples to ensure all documented keys
are mapped before constructing config.hooks.
- Around line 414-421: When building budget_kwargs for AgentBudgetConfig from
cf_config.agent, ensure min_iterations is adjusted when max_iterations is
lowered: if "max_iterations" is present and "min_iterations" is not explicitly
provided, set budget_kwargs["min_iterations"] to the same (or to no greater
than) budget_kwargs["max_iterations"] so that
base_iterations/max_iterations/min_iterations remain consistent; update the
block that currently sets budget_kwargs["max_iterations"] and
budget_kwargs["base_iterations"] (the cf_config.agent handling that creates
config.agent_budget = AgentBudgetConfig(**budget_kwargs)) to also set
budget_kwargs["min_iterations"] accordingly.
In `@tests/core/test_codeframe_config.py`:
- Around line 11-17: Remove the unused imports AgentPreferences and
CodeframeConfig from the import list in tests/core/test_codeframe_config.py so
Ruff no longer flags F401; keep only the symbols actually used (e.g.,
load_preferences, parse_codeframe_md, get_codeframe_config) in the from
codeframe.core.agents_config import (...) statement.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3d53c3cc-3539-497a-a654-e0bbbd218caf
📒 Files selected for processing (6)
codeframe/cli/app.pycodeframe/core/agents_config.pycodeframe/core/config.pytests/cli/test_init_generate_config.pytests/core/test_codeframe_config.pytests/core/test_config_codeframe_fallback.py
| # Generate CODEFRAME.md if requested | ||
| if generate_config: | ||
| config_content = _generate_codeframe_md( | ||
| tech_stack=final_tech_stack or "", | ||
| ) | ||
| config_path = repo_path / "CODEFRAME.md" | ||
| config_path.write_text(config_content) | ||
| console.print(" Generated: CODEFRAME.md") |
There was a problem hiding this comment.
Avoid clobbering an existing CODEFRAME.md.
Re-running cf init --generate-config currently truncates any checked-in CODEFRAME.md without confirmation. Since that file now carries both runtime settings and agent instructions, this should fail fast or require an explicit force flag.
🛠️ Suggested guard
# Generate CODEFRAME.md if requested
if generate_config:
- config_content = _generate_codeframe_md(
- tech_stack=final_tech_stack or "",
- )
config_path = repo_path / "CODEFRAME.md"
- config_path.write_text(config_content)
+ if config_path.exists():
+ console.print("[red]Error:[/red] CODEFRAME.md already exists. Use --force to overwrite it.")
+ raise typer.Exit(1)
+ config_content = _generate_codeframe_md(tech_stack=final_tech_stack or "")
+ config_path.write_text(config_content, encoding="utf-8")
console.print(" Generated: CODEFRAME.md")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@codeframe/cli/app.py` around lines 180 - 187, The current generation code
unconditionally overwrites CODEFRAME.md; modify the logic around
generate_config/_generate_codeframe_md so that before calling
config_path.write_text you check if (repo_path / "CODEFRAME.md").exists() and if
so fail fast (raise an error or console.print and exit) unless an explicit force
flag is provided; add or wire a boolean force parameter (e.g., --force or
force_generate) into the CLI handler and use it in this branch to allow
overwriting when true, otherwise do not clobber the existing file and return a
clear message referencing CODEFRAME.md and the --force option.
| # Detect gates from tech stack | ||
| gates: list[str] = [] | ||
| if tech_stack: | ||
| ts_lower = tech_stack.lower() | ||
| if "python" in ts_lower or "pytest" in ts_lower: | ||
| gates.extend(["ruff", "pytest"]) | ||
| elif "typescript" in ts_lower or "jest" in ts_lower: | ||
| gates.extend(["eslint", "jest"]) | ||
| if gates: | ||
| yaml_section["gates"] = gates |
There was a problem hiding this comment.
Infer gates for every detected stack, not just the first one.
_detect_tech_stack() can return mixed or JavaScript-only descriptions, but the if/elif chain only emits the first matching gate set and ignores JS stacks that do not mention typescript. The generated CODEFRAME.md ends up incomplete for monorepos and plain JS repos.
🛠️ Suggested fix
# Detect gates from tech stack
gates: list[str] = []
if tech_stack:
ts_lower = tech_stack.lower()
if "python" in ts_lower or "pytest" in ts_lower:
gates.extend(["ruff", "pytest"])
- elif "typescript" in ts_lower or "jest" in ts_lower:
- gates.extend(["eslint", "jest"])
+ if any(token in ts_lower for token in ("typescript", "javascript", "jest", "vitest", "eslint")):
+ gates.append("eslint")
+ if "jest" in ts_lower:
+ gates.append("jest")
+ elif "vitest" in ts_lower:
+ gates.append("vitest")
+ gates = list(dict.fromkeys(gates))
if gates:
yaml_section["gates"] = gates📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Detect gates from tech stack | |
| gates: list[str] = [] | |
| if tech_stack: | |
| ts_lower = tech_stack.lower() | |
| if "python" in ts_lower or "pytest" in ts_lower: | |
| gates.extend(["ruff", "pytest"]) | |
| elif "typescript" in ts_lower or "jest" in ts_lower: | |
| gates.extend(["eslint", "jest"]) | |
| if gates: | |
| yaml_section["gates"] = gates | |
| # Detect gates from tech stack | |
| gates: list[str] = [] | |
| if tech_stack: | |
| ts_lower = tech_stack.lower() | |
| if "python" in ts_lower or "pytest" in ts_lower: | |
| gates.extend(["ruff", "pytest"]) | |
| if any(token in ts_lower for token in ("typescript", "javascript", "jest", "vitest", "eslint")): | |
| gates.append("eslint") | |
| if "jest" in ts_lower: | |
| gates.append("jest") | |
| elif "vitest" in ts_lower: | |
| gates.append("vitest") | |
| gates = list(dict.fromkeys(gates)) | |
| if gates: | |
| yaml_section["gates"] = gates |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@codeframe/cli/app.py` around lines 353 - 362, The current gate inference uses
an if/elif chain so only the first match is emitted; modify the logic around
tech_stack/ts_lower and gates so each detected runtime/framework adds its gates
independently (replace the if/elif with separate if checks), and add checks for
plain JavaScript indicators like "javascript" or "node" (so eslint/jest are
added when TypeScript isn't mentioned). Ensure you still append results into the
gates list and assign yaml_section["gates"] when non-empty.
| # Map hooks | ||
| if cf_config.hooks: | ||
| valid_hook_fields = {f.name for f in HooksConfig.__dataclass_fields__.values()} | ||
| filtered = {k: v for k, v in cf_config.hooks.items() if k in valid_hook_fields} | ||
| config.hooks = HooksConfig(**filtered) |
There was a problem hiding this comment.
Normalize the documented hook names before filtering.
parse_codeframe_md() exposes hook keys like after_task and on_failure, but this branch only preserves exact HooksConfig field names. In the CODEFRAME.md fallback path those hooks are silently dropped, so the runtime config never executes them.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@codeframe/core/config.py` around lines 399 - 403, The hook keys produced by
parse_codeframe_md (e.g., after_task, on_failure) are not being normalized
before creating HooksConfig, so documented hooks get dropped; update the
cf_config.hooks handling to first normalize/translate documented hook names to
the actual HooksConfig field names (for example by building a mapping from
documented names -> dataclass field names or by applying a deterministic rename
function), then filter using HooksConfig.__dataclass_fields__ and instantiate
config.hooks = HooksConfig(**filtered); touch the cf_config.hooks branch and use
the HooksConfig symbol and parse_codeframe_md examples to ensure all documented
keys are mapped before constructing config.hooks.
| if cf_config.agent: | ||
| budget_kwargs: dict[str, Any] = {} | ||
| if "max_iterations" in cf_config.agent: | ||
| budget_kwargs["max_iterations"] = cf_config.agent["max_iterations"] | ||
| # Also set base_iterations to match (they're conceptually the same) | ||
| budget_kwargs["base_iterations"] = cf_config.agent["max_iterations"] | ||
| if budget_kwargs: | ||
| config.agent_budget = AgentBudgetConfig(**budget_kwargs) |
There was a problem hiding this comment.
Keep min_iterations in sync with a lowered max_iterations.
A value like agent.max_iterations: 5 produces base_iterations=5, max_iterations=5, and the default min_iterations=15. That makes the fallback EnvironmentConfig internally invalid before it ever reaches runtime validation.
🛠️ Suggested fix
if cf_config.agent:
budget_kwargs: dict[str, Any] = {}
if "max_iterations" in cf_config.agent:
- budget_kwargs["max_iterations"] = cf_config.agent["max_iterations"]
- # Also set base_iterations to match (they're conceptually the same)
- budget_kwargs["base_iterations"] = cf_config.agent["max_iterations"]
+ max_iterations = int(cf_config.agent["max_iterations"])
+ default_min_iterations = AgentBudgetConfig().min_iterations
+ budget_kwargs["max_iterations"] = max_iterations
+ budget_kwargs["base_iterations"] = max_iterations
+ budget_kwargs["min_iterations"] = min(default_min_iterations, max_iterations)
if budget_kwargs:
config.agent_budget = AgentBudgetConfig(**budget_kwargs)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@codeframe/core/config.py` around lines 414 - 421, When building budget_kwargs
for AgentBudgetConfig from cf_config.agent, ensure min_iterations is adjusted
when max_iterations is lowered: if "max_iterations" is present and
"min_iterations" is not explicitly provided, set budget_kwargs["min_iterations"]
to the same (or to no greater than) budget_kwargs["max_iterations"] so that
base_iterations/max_iterations/min_iterations remain consistent; update the
block that currently sets budget_kwargs["max_iterations"] and
budget_kwargs["base_iterations"] (the cf_config.agent handling that creates
config.agent_budget = AgentBudgetConfig(**budget_kwargs)) to also set
budget_kwargs["min_iterations"] accordingly.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/core/test_codeframe_config.py (1)
274-307: Consider adding a test for parent directory walking.The tests cover the basic cases well. However,
get_codeframe_config()walks up the directory tree to find CODEFRAME.md (per lines 556-566 inagents_config.py). A test verifying that a CODEFRAME.md in a parent directory is found when called from a subdirectory would improve coverage of this behavior.💡 Example additional test
def test_get_codeframe_config_finds_parent(self): """Finds CODEFRAME.md in parent directory when searching from subdirectory.""" with TemporaryDirectory() as tmpdir: root = Path(tmpdir) subdir = root / "src" / "module" subdir.mkdir(parents=True) codeframe_md = root / "CODEFRAME.md" codeframe_md.write_text("""\ --- engine: plan --- """) config = get_codeframe_config(subdir) assert config is not None assert config.engine == "plan"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/core/test_codeframe_config.py` around lines 274 - 307, Add a new unit test in TestGetCodeframeConfig that verifies get_codeframe_config walks parent directories: create a temporary root with a CODEFRAME.md (containing at least engine: plan), make a nested subdirectory (e.g., root/src/module), call get_codeframe_config(subdir) and assert it returns a config with engine == "plan"; name the test method test_get_codeframe_config_finds_parent so it clearly targets the parent-walking behavior and references CODEFRAME.md and get_codeframe_config.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@tests/core/test_codeframe_config.py`:
- Around line 274-307: Add a new unit test in TestGetCodeframeConfig that
verifies get_codeframe_config walks parent directories: create a temporary root
with a CODEFRAME.md (containing at least engine: plan), make a nested
subdirectory (e.g., root/src/module), call get_codeframe_config(subdir) and
assert it returns a config with engine == "plan"; name the test method
test_get_codeframe_config_finds_parent so it clearly targets the parent-walking
behavior and references CODEFRAME.md and get_codeframe_config.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bcb51c82-5df4-4f35-a376-1b4d191577ab
📒 Files selected for processing (1)
tests/core/test_codeframe_config.py
Summary
Closes #398
CODEFRAME.md>AGENTS.md>CLAUDE.mdin load_preferences()load_environment_config()falls back to CODEFRAME.md when no.codeframe/config.yamlcf init --generate-configscaffolds a starter CODEFRAME.md with auto-detected settingsFiles Changed
core/agents_config.pycore/config.pycli/app.pyTest plan
cf init --generate-configproduces valid CODEFRAME.mdSummary by CodeRabbit
New Features
Tests